package sk.stuba.fiit.clientApp.service.impl;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.cert.CRLException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CRL;
import java.security.cert.X509CRLEntry;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import javax.xml.bind.JAXBElement;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.bouncycastle.tsp.TimeStampResponse;
import org.etsi.uri._01903.v1_3.CertificateValuesType;
import org.etsi.uri._01903.v1_3.EncapsulatedPKIDataType;
import org.etsi.uri._01903.v1_3.QualifyingPropertiesType;
import org.etsi.uri._01903.v1_3.XAdESTimeStampType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.stereotype.Service;
import org.springframework.ws.client.core.WebServiceTemplate;
import org.w3._2000._09.xmldsig.ObjectType;
import org.w3._2000._09.xmldsig.ReferenceType;
import org.w3._2000._09.xmldsig.SignedInfoType;
import org.w3._2000._09.xmldsig.X509DataType;

import sk.stuba.fiit.clientApp.cvo.VerifyPanelDataCVO;
import sk.stuba.fiit.clientApp.service.CertificateService;
import sk.stuba.fiit.clientApp.service.CrlService;
import sk.stuba.fiit.clientApp.service.CryptographyService;
import sk.stuba.fiit.clientApp.service.StorageService;
import sk.stuba.fiit.clientApp.service.bussiness.XMLSignerCreator;
import sk.stuba.fiit.clientApp.singeltons.ApplicationContextSingelton;
import sk.stuba.fiit.clientApp.util.Util;

import sk.stuba.fiit.ltasys.ws.schema.beans.ArchivePackageType;
import sk.stuba.fiit.ltasys.ws.schema.beans.ArchiveResponse;
import sk.stuba.fiit.ltasys.ws.schema.beans.EvidenceRecordSequenceType;
import sk.stuba.fiit.ltasys.ws.schema.beans.EvidenceRecordType;
import sk.stuba.fiit.ltasys.ws.schema.beans.SignatureRequest;
import sk.stuba.fiit.ltasys.ws.schema.beans.UriRefType;
import sun.security.x509.X509CRLImpl;

@Service("archivVerifier")
public class ArchivVerifierService {	
	@Autowired
	private CryptographyService cryptographyService;
	@Autowired 
	private StorageService storageService;


	public boolean verifySignatureComplex(VerifyPanelDataCVO data) throws Exception {
			verifyEvidenceRecord(data);
			verifyOriginalBaseSignature(data);
			return true;
	}

	private boolean verifyOriginalBaseSignature(VerifyPanelDataCVO data)throws Exception {
		Source source = new StreamSource(new File(data.getPath())+File.separator+"Signature.xml");
		SignatureRequest archiveRequest = (SignatureRequest)SignerServiceImpl.controler.unmarshal(source);
		
		File dir = new File(data.getPath());
		List<File> dirFiles =new ArrayList<File>();
		for (File file : dir.listFiles()) {
			dirFiles.add(new File( data.getPath()+File.separator+file.getName()));
		}
		data.setFiles(dirFiles);
		
		SignedInfoType sit;
		try{
			sit = XMLSignerCreator.canonicalize(archiveRequest.getXades().get(0).getSignature().getSignedInfo());
		}catch(Exception e){
			sit = archiveRequest.getXades().get(0).getSignature().getSignedInfo();
		}
		
		List<ReferenceType> reference = sit.getReference();
	
		byte []hashRef = null;
		byte []hashDat = null;
	
		List<Object> hashesToCompute = new ArrayList<Object>();
		for (ReferenceType referenceType : reference) {
			if(!(referenceType.getType()!=null && referenceType.getType().equals(Util.SIGNED_PROPERTIES_TYPE))){
				List<File> files =data.getFiles();
				for(int i = 0; i<files.size();i++){
					if(referenceType.getURI().equals(files.get(i).getName())){
						hashRef = referenceType.getDigestValue();
						hashDat = cryptographyService.calculateHash(storageService.readFile(files.get(i)), referenceType.getDigestMethod().getAlgorithm());				
					}
				}
			}else{
				StringWriter writer = new StringWriter();
				Result result = new StreamResult(writer);
				ObjectType ot = archiveRequest.getXades().get(0).getSignature().getObject().get(0);
				JAXBElement<QualifyingPropertiesType> jaxbelement = (JAXBElement<QualifyingPropertiesType>) ot.getContent().get(0);
				QualifyingPropertiesType qpt = jaxbelement.getValue();
				SignerServiceImpl.controler.marshal(qpt.getSignedProperties(), result);
				hashDat = cryptographyService.calculateHash(writer.toString().getBytes(), referenceType.getDigestMethod().getAlgorithm());
				hashRef = referenceType.getDigestValue();;
			}
			hashesToCompute.add(hashDat);
		}
		String signAlgorithm = archiveRequest.getXades().get(0).getSignature().getSignedInfo().getSignatureMethod().getAlgorithm();
		
		
		
		
		
		@SuppressWarnings("unchecked")
		JAXBElement<X509DataType>  jaxbdataType=  (JAXBElement<X509DataType>) archiveRequest.getXades().get(0).getSignature().getKeyInfo().getContent().get(0);
		X509DataType datatype = jaxbdataType.getValue(); 
		@SuppressWarnings("unchecked")
		JAXBElement<CertificateValuesType> jaxbCertValType = (JAXBElement<CertificateValuesType>) datatype.getX509IssuerSerialOrX509SKIOrX509SubjectName().get(0);
		CertificateValuesType certValType = jaxbCertValType.getValue();
		EncapsulatedPKIDataType epkidt = (EncapsulatedPKIDataType) certValType.getEncapsulatedX509CertificateOrOtherCertificate().get(0);
		CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
		InputStream in = new ByteArrayInputStream(epkidt.getValue());		
		X509Certificate certificate = (X509Certificate)certFactory.generateCertificate(in);
	
		if(!cryptographyService.verifieSignature(signAlgorithm, certificate, hashesToCompute, archiveRequest.getXades().get(0).getSignature().getSignatureValue().getValue()))
			throw new Exception("Verification faild");
		
		ObjectType ot = archiveRequest.getXades().get(0).getSignature().getObject().get(0);
		@SuppressWarnings("unchecked")
		JAXBElement<QualifyingPropertiesType> jaxbelement = (JAXBElement<QualifyingPropertiesType>) ot.getContent().get(0);
		QualifyingPropertiesType qpt = jaxbelement.getValue();
		
		
		
		EncapsulatedPKIDataType edtallDataTS = (EncapsulatedPKIDataType)qpt.getSignedProperties().getSignedDataObjectProperties().getAllDataObjectsTimeStamp().get(0).getEncapsulatedTimeStampOrXMLTimeStamp().get(0);
		TimeStampResponse allDataTimestamp = new TimeStampResponse(edtallDataTS.getValue());
		
		int size = 0;
		for(int i=0; i < reference.size(); i++){
			if(reference.get(i).getType() == null || !reference.get(i).getType().equals(Util.SIGNED_PROPERTIES_TYPE)){
				size += reference.get(i).getDigestValue().length;
			}
		}
		byte[] hashData = new byte[size];
		int index=0;
		for(int i=0; i < reference.size(); i++){
			if(reference.get(i).getType() == null || !reference.get(i).getType().equals(Util.SIGNED_PROPERTIES_TYPE)){
				byte[]tmp = reference.get(i).getDigestValue();
				for(int j = 0; j<tmp.length; j++){
					hashData[index++]=tmp[j];
				}
			}
		}
		
		MessageDigest hash = MessageDigest.getInstance("SHA1", "BC");
		hash.update(hashData);
		if(!Arrays.equals(hash.digest(),allDataTimestamp.getTimeStampToken().getTimeStampInfo().getMessageImprintDigest())){
			throw new Exception("Incorrect allDataObjectTimestamp");
		}

		EncapsulatedPKIDataType edtinitSigDataTS;
		TimeStampResponse initialSignatureTimestamp;
		if(qpt.getSignedProperties().getSignedDataObjectProperties().getAllDataObjectsTimeStamp().size()>1){
			index = qpt.getSignedProperties().getSignedDataObjectProperties().getAllDataObjectsTimeStamp().size()-1;
			edtinitSigDataTS = (EncapsulatedPKIDataType)qpt.getSignedProperties().getSignedDataObjectProperties().getAllDataObjectsTimeStamp().get(index).getEncapsulatedTimeStampOrXMLTimeStamp().get(0);		
			initialSignatureTimestamp = new TimeStampResponse(edtinitSigDataTS.getValue());
		}else{
			initialSignatureTimestamp = allDataTimestamp;
		}
		
		hash.reset();
		hash = MessageDigest.getInstance("SHA1", "BC");
		hash.update(hashData);
		if(!Arrays.equals(hash.digest(),initialSignatureTimestamp.getTimeStampToken().getTimeStampInfo().getMessageImprintDigest())){
			throw new Exception("Incorrect initialTimestamp");
		}
		
		@SuppressWarnings("unchecked")
		JAXBElement<XAdESTimeStampType> xdssigDataTS = (JAXBElement<XAdESTimeStampType>) qpt.getUnsignedProperties().getUnsignedSignatureProperties().getCounterSignatureOrSignatureTimeStampOrCompleteCertificateRefs().get(0);		
		EncapsulatedPKIDataType edtSigDataTS = (EncapsulatedPKIDataType) xdssigDataTS.getValue().getEncapsulatedTimeStampOrXMLTimeStamp().get(0);
		TimeStampResponse signatureTimestamp =  new TimeStampResponse(edtSigDataTS.getValue());

		hash.reset();
		hash = MessageDigest.getInstance("SHA1", "BC");
		hash.update(archiveRequest.getXades().get(0).getSignature().getSignatureValue().getValue());
		if(!Arrays.equals(hash.digest(),signatureTimestamp.getTimeStampToken().getTimeStampInfo().getMessageImprintDigest())){
			throw new Exception("Incorrect SignatureTimestamp");
		}
		
		System.out.println();
		System.out.println("Ideme na to");
		return true;
	}
	

	
	private void verifyEvidenceRecord(VerifyPanelDataCVO data) throws Exception{
		Source source = new StreamSource(new File(data.getPath())+File.separator+"EvidenceRecord.xml");
		ArchivePackageType archivPackage = (ArchivePackageType)SignerServiceImpl.controler.unmarshal(source);
		ArrayList<byte[]> userdata = new ArrayList<byte[]>();
		EvidenceRecordType ert = archivPackage.getEvidenceRecordSequence().getEvidenceRecord().get(0);
		
		for (int i = 0; i< ert.getDataRef().getURI().size(); i++){
			UriRefType uri = ert.getDataRef().getURI().get(i);
			File file = new File(data.getPath()+File.separator+uri.getURI());
			FileInputStream fis = new FileInputStream(file);
			byte[] binartData = new byte[(int) file.length()];
			fis.read(binartData);
			fis.close();
			userdata.add(binartData);
		}
		verifieArchiveSignature(archivPackage,userdata);
	}

	
	private List<X509Certificate> processCertificates(
			List<Object> encapsulatedCert) throws Exception {
		CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
		List <X509Certificate> certificate = new ArrayList<X509Certificate>();
		for (Object objCert: encapsulatedCert) {
			EncapsulatedPKIDataType encCert =  (EncapsulatedPKIDataType) objCert;
			InputStream in = new ByteArrayInputStream(encCert.getValue());		
			certificate.add((X509Certificate) certFactory.generateCertificate(in));	
			
		}		
		return certificate;
	}
	
	private List<X509CRL> processCrls(
			List<EncapsulatedPKIDataType> encapsulatedCRLValue) throws Exception {
		
		List <X509CRL> crls = new ArrayList<X509CRL>();
		for (Object objCert: encapsulatedCRLValue) {
			EncapsulatedPKIDataType encCert =  (EncapsulatedPKIDataType) objCert;	
			crls.add(new X509CRLImpl(encCert.getValue()));
			
		}
		return crls;
	}
	
	
	@Autowired
	private CertificateService certificateService;
	@Autowired
	private CrlService crlService;
	
private void verifieArchiveSignature(ArchivePackageType archiv, ArrayList<byte[]> userdata) throws Exception{
		
		List<EvidenceRecordType> list = (List<EvidenceRecordType>) archiv.getEvidenceRecordSequence().getEvidenceRecord();
		Set<X509Certificate> allCertificates = new HashSet<X509Certificate>();
		
		EvidenceRecordType oldActual = null;
		for (int i = list.size()-1; i >= 0; i--) {
			EvidenceRecordType actual = list.get(i);
			
			byte[] hash = comuteHashRoot(archiv, actual,userdata);
			byte[] tstData = ((EncapsulatedPKIDataType) actual.getProtectionTimestamp().getEncapsulatedTimeStampOrXMLTimeStamp().get(0)).getValue();
			TimeStampResponse tsr = new TimeStampResponse(tstData); 
			byte[] tstHash = tsr.getTimeStampToken().getTimeStampInfo().getMessageImprintDigest();
			
		
			if(list.size()-1 == i){ 
				X509Certificate certificate = certificateService.getCertificateBySerialNum(""+tsr.getTimeStampToken().getTimeStampInfo().getSerialNumber());
				X509CRL crl = crlService.getActualCRL();

				X509CRLEntry entry = crl.getRevokedCertificate(certificate);
				if(entry != null ){
					throw new Exception("Revocated certificate");
				}
				tsr.getTimeStampToken().validate(certificate, "BC");
				allCertificates.add(certificate);
				oldActual = actual;
			}else{
				List<X509Certificate> certificates = processCertificates(oldActual.getCertificateValues().getEncapsulatedX509CertificateOrOtherCertificate());
				List<X509CRL> crls = processCrls(oldActual.getRevocationValues().getCRLValues().getEncapsulatedCRLValue());				
				X509Certificate cerificate = null;
				BigInteger snCert = tsr.getTimeStampToken().getTimeStampInfo().getSerialNumber();
				for (X509Certificate certEntity : certificates) {
					if(certEntity.getSerialNumber().equals(snCert)){		
						cerificate =  certEntity;	
					}
				}
				if(cerificate == null){
					throw new Exception("Certificate is not in evidence record");
				}
				X509CRL crl = crls.get(0);
				
				X509CRLEntry entry = crl.getRevokedCertificate(cerificate);
				if(entry != null ){
					throw new Exception("Revocated certificate");
				}
				tsr.getTimeStampToken().validate(cerificate, "BC");
				allCertificates.add(cerificate);
				oldActual = actual;
			}
		}
		
		for (X509Certificate x509Certificate : allCertificates) {
			createAndValidateCertificatePath(x509Certificate);		
		}

	}

	private void createAndValidateCertificatePath( X509Certificate x509Certificate) throws Exception {
		ArrayList<X509Certificate> certificateChain = new ArrayList<X509Certificate>();
		certificateChain.add(x509Certificate);
		while(!x509Certificate.getIssuerDN().equals(x509Certificate.getSubjectDN())){
			X509Certificate cert = certificateService.getCertificateByIssuer(x509Certificate.getIssuerDN().toString());
			certificateChain.add(cert);
			x509Certificate = cert;
		}
		
		for (int i = 0; i < certificateChain.size()-1; i++) {
			certificateChain.get(i).verify(certificateChain.get(i+1).getPublicKey());
		}
		certificateChain.get(certificateChain.size()-1).verify(certificateChain.get(certificateChain.size()-1).getPublicKey());
	}

	private byte [] comuteHashRoot(ArchivePackageType archiv, EvidenceRecordType evidence, ArrayList<byte[]> userdata) throws Exception{
		List<EvidenceRecordType> list = (List<EvidenceRecordType>) archiv.getEvidenceRecordSequence().getEvidenceRecord();
//		
//		//najstarsi zaznam
		if(list.get(0).equals(evidence) ){
			 List<byte[]> referenceData = new ArrayList<byte[]>();

			 for(int i  = 0 ; i< userdata.size(); i++){
				 referenceData.add(userdata.get(i));			 
			 }
			 
			 byte[] hashReference = computeNodeHash(referenceData); 
			 
////			 pridam aktualny certifikat
			 List<byte[]> protectionData = new ArrayList<byte[]>();
			 for (X509Certificate certificate : processCertificates(evidence.getCertificateValues().getEncapsulatedX509CertificateOrOtherCertificate())) {
				 protectionData.add(certificate.getEncoded());
			 }
			 for (X509CRL crl: processCrls(evidence.getRevocationValues().getCRLValues().getEncapsulatedCRLValue()) ){
				 protectionData.add(crl.getEncoded());
			 }			 
//			 
			 byte[] hashProtectionData = computeNodeHash(protectionData);			 
			 List<byte[]> timestampData = new ArrayList<byte[]>();
				timestampData.add(hashReference);
				timestampData.add(hashProtectionData);
//				
			return computeNodeHash(timestampData);
		}else{
			byte[] hashReference = comuteHashRoot(archiv, list.get(list.indexOf(evidence)-1),userdata);
//			 
			 List<byte[]> protectionData = new ArrayList<byte[]>();
			 for (X509Certificate certificate : processCertificates(evidence.getCertificateValues().getEncapsulatedX509CertificateOrOtherCertificate())) {
				 protectionData.add(certificate.getEncoded());
			 }
			 for (X509CRL crl: processCrls(evidence.getRevocationValues().getCRLValues().getEncapsulatedCRLValue()) ){
				 protectionData.add(crl.getEncoded());
			  }			
			 
			 byte[] tstData = ((EncapsulatedPKIDataType) list.get(list.indexOf(evidence)-1).getProtectionTimestamp().getEncapsulatedTimeStampOrXMLTimeStamp().get(0)).getValue();
			TimeStampResponse tsr = new TimeStampResponse(tstData); 
			 protectionData.add(tsr.getEncoded());
			 
			 byte[] hashProtectionData = computeNodeHash(protectionData);
			
			 List<byte[]> timestampData = new ArrayList<byte[]>();
			 timestampData.add(hashReference);
			 timestampData.add(hashProtectionData);	
			 return computeNodeHash(timestampData);
		}
	}
	
	private static byte[] computeNodeHash(List<byte[]> unorderedByteList) throws Exception{
		TreeSet<String> orderedData = new TreeSet<String>();
		MessageDigest hash = MessageDigest.getInstance("SHA", "BC");		
		
		for (byte[] data : unorderedByteList) {
			hash.reset();
			hash.update(data);
			orderedData.add(new String(hash.digest()));
		}
		hash.reset();
		for (String hashData : orderedData) {
			hash.update(hashData.getBytes());
		}
		byte [] result = hash.digest();
		System.out.println(new String(result));
		return result;
	}
	
}
